package joe;

import java.util.function.LongConsumer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public interface Render {
	@SuppressWarnings("CanBeFinal")
	class InitParams {
		public @NotNull String title = "Joe";
		public boolean fullWindow;
		public boolean keepAspectRatio = true;
		public int width = 1280;
		public int height = 720;
		public int swapInterval = 1;
		public float clearRed = 0.25f;
		public float clearGreen = 0.25f;
		public float clearBlue = 0.25f;
		public float clearAlpha = 0;
	}

	void init(@NotNull InitParams params);

	boolean isRunning();

	void beginRender();

	int getWindowWidth();

	int getWindowHeight();

	/**
	 * @param y 0:bottom
	 */
	void setViewport(int x, int y, int width, int height);

	/**
	 * @return old status, default: true
	 */
	boolean setCullBack(boolean cullBack);

	/**
	 * @return old status, default: true
	 */
	boolean setDepthTest(boolean depthTest);

	/**
	 * @return old status, default: true
	 */
	boolean setDepthMask(boolean depthMask);

	/**
	 * @param factor default/disable:0
	 * @param units  default/disable:0
	 */
	void setDepthOffset(float factor, float units);

	/**
	 * @return old status, default: true
	 */
	boolean setBlend(boolean blend);

	void endRender();

	void handleEvent();

	void present();

	void destroy();

	interface Buffer {
		/**
		 * @return byte size of the buffer
		 */
		long dataSize();

		/**
		 * @param offset   byte offset of the buffer
		 * @param dataPtr  can NOT be null
		 * @param dataSize byte size in dataPtr, can be larger than last data size if offset = 0
		 */
		void putData(long offset, long dataPtr, long dataSize);
	}

	interface VertexBuffer extends Buffer {
	}

	/**
	 * @param format         b/ub/s/us/i/ui/x/f/h/_ + count (upper for normalized, _ for byte-align), e.g. "f3UB4f2"
	 * @param vertexDataPtr  can be null for empty
	 * @param vertexDataSize byte size in vertexDataPtr
	 */
	@NotNull VertexBuffer createVertexBuffer(@NotNull String format, int stride,
											 long vertexDataPtr, long vertexDataSize);

	interface VertexIndex extends Buffer {
		/**
		 * @return 1:R; 2:RG; 3:RGB; 4:RGBA; 5:depth16(ushort); 6:depth24(uint); 7:depth32f(float)
		 */
		int depth();
	}

	/**
	 * @param depth         1,2,4
	 * @param indexDataPtr  can NOT be null
	 * @param indexDataSize byte size in indexDataPtr
	 */
	@NotNull VertexIndex loadVertexIndex(int depth, long indexDataPtr, long indexDataSize);

	interface Mesh {
		@NotNull VertexBuffer getVertexBuffer();

		@Nullable VertexBuffer getInstanceBuffer();

		@Nullable VertexIndex getVertexIndex();

		void setVertexIndex(@Nullable VertexIndex vertexIndex);
	}

	int POINTS = 0;         // v0; v1; v2; v3; ...
	int LINES = 1;          // v0,v1; v2,v3; v4,v5; ...
	int LINE_LOOP = 2;      // v0,v1; v1,v2; v2,v3; ...; vn,v0
	int LINE_STRIP = 3;     // v0,v1; v1,v2; v2,v3; ...
	int TRIANGLES = 4;      // v0,v1,v2; v3,v4,v5; v6,v7,v8; ...
	int TRIANGLE_STRIP = 5; // v0,v1,v2; v2,v1,v3; v2,v3,v4; v4,v3,v5; ...
	int TRIANGLE_FAN = 6;   // v0,v1,v2; v0,v2,v3; v0,v3,v4; v0,v4,v5; ...

	/**
	 * @param drawMode {@link #POINTS} {@link #LINES} {@link #TRIANGLES} ...
	 */
	@NotNull Mesh createMesh(int drawMode, @NotNull VertexBuffer vertexBuffer, @Nullable VertexIndex indexData,
							 @Nullable VertexBuffer instanceBuffer);

	default @NotNull Mesh createMesh(int drawMode, @NotNull VertexBuffer vertexBuffer,
									 @Nullable VertexIndex indexData) {
		return createMesh(drawMode, vertexBuffer, indexData, null);
	}

	default @NotNull Mesh createMesh(int drawMode, @NotNull VertexBuffer vertexBuffer) {
		return createMesh(drawMode, vertexBuffer, null, null);
	}

	interface UploadPixelBuffer extends Buffer {
		/**
		 * use for large flow texture pixel data uploading
		 *
		 * @param offset   byte offset of mapped pixel data
		 * @param dataSize byte size of mapped pixel data, 0 for the entire left size
		 * @param handler  write pixel data to the pointer parameter, which is only available in the handler::accept
		 */
		void upload(long offset, long dataSize, boolean invalidLastUploadRange, @NotNull LongConsumer handler);
	}

	/**
	 * @param pixelDataSize byte size
	 */
	@NotNull UploadPixelBuffer createUploadPixelBuffer(long pixelDataSize);

	interface DownloadPixelBuffer extends Buffer {
		/**
		 * use for large flow texture or frame pixel data downloading
		 *
		 * @param offset   byte offset of mapped pixel data
		 * @param dataSize byte size of mapped pixel data, 0 for the entire left size
		 * @param handler  read pixel data from the pointer parameter, which is only available in the handler::accept
		 */
		void download(long offset, long dataSize, @NotNull LongConsumer handler);
	}

	/**
	 * @param pixelDataSize byte size
	 */
	@NotNull DownloadPixelBuffer createDownloadPixelBuffer(long pixelDataSize);

	interface Texture2D {
		int width();

		int height();

		/**
		 * @return 1:R; 2:RG; 3:RGB; 4:RGBA; 5:depth16(ushort); 6:depth24(uint); 7:depth32f(float)
		 */
		int depth();

		/**
		 * @return byte size
		 */
		default long dataSize() {
			int size = switch (depth()) {
				case 1 -> 1;
				case 2, 5 -> 2;
				case 3 -> 3;
				case 4, 6, 7 -> 4;
				default -> throw new IllegalArgumentException("invalid depth: " + depth());
			};
			return (long)width() * height() * size;
		}

		/**
		 * @param y          0:top
		 * @param texDataPtr unsigned bytes, size=alignUp(width*depthSize,4)*height, from top to down, can NOT be null
		 */
		void putData(int x, int y, int width, int height, long texDataPtr);

		/**
		 * @param y 0:top
		 */
		void putData(int x, int y, int width, int height, @NotNull UploadPixelBuffer buffer);
	}

	//@formatter:off
	int MIN_NEAREST_FILTER = 0x0001; // or linear filter
	int MAG_NEAREST_FILTER = 0x0010; // or linear filter
	int CLAMP_TO_EDGE      = 0x0100; // or repeat
	//@formatter:on

	/**
	 * @param depth      1:R; 2:RG; 3:RGB; 4:RGBA; 5:depth16(ushort); 6:depth24(uint); 7:depth32f(float)
	 * @param flags      {@link #MIN_NEAREST_FILTER} {@link #MAG_NEAREST_FILTER} {@link #CLAMP_TO_EDGE}
	 * @param texDataPtr unsigned bytes, size=alignUp(width*depthSize,4)*height, from top to down, can be null for empty
	 */
	@NotNull Texture2D createTexture2D(int width, int height, int depth, int flags, long texDataPtr);

	interface Frame {
		int MAX_COLOR_BUFFERS = 4;

		/**
		 * @param index [0,MAX_COLOR_BUFFERS):ColorBuffer; -1:DepthBuffer
		 * @param depth 1:R; 2:RG; 3:RGB; 4:RGBA; 5:depth16(ushort); 6:depth24(uint); 7:depth32f(float)
		 */
		void createBuffer(int index, int width, int height, int depth);

		void bindTexture(int index, @Nullable Texture2D tex);

		/**
		 * @param index [0,MAX_COLOR_BUFFERS):ColorBuffer; -1:DepthBuffer
		 */
		@Nullable Texture2D getTexture(int index);

		/**
		 * @param index [0,MAX_COLOR_BUFFERS]; other:don't use back buffer, default:-1
		 */
		void setBackBufferIndex(int index);

		int getBackBufferIndex();

		void use();

		/**
		 * only for latest 'create*,bind,use' frame
		 *
		 * @param index [0,MAX_COLOR_BUFFERS); don't clear any color buffer for others
		 * @param depth [0,1]; don't clear depth buffer for others
		 */
		void clear(int index, float red, float green, float blue, float alpha, float depth);

		void unuse();
	}

	@NotNull Frame createFrame();

	//@formatter:off
	int COPY_DEPTH     = 0x0001;
	int NEAREST_FILTER = 0x0010; // or linear filter
	//@formatter:on

	/**
	 * @param srcFrame null:BackBuffer
	 * @param srcIndex [0,MAX_COLOR_BUFFERS):colorIndex, ignore if using BackBuffer
	 * @param flags    {@link #COPY_DEPTH} {@link #NEAREST_FILTER}
	 * @param srcY     0:top
	 */
	void copyFrame(@Nullable Frame srcFrame, @Nullable Frame dstFrame, int srcIndex, int dstIndex, int flags,
				   int srcX, int srcY, int srcWidth, int srcHeight,
				   int dstX, int dstY, int dstWidth, int dstHeight);

	/**
	 * @param frame   null:BackBuffer
	 * @param index   [0,MAX_COLOR_BUFFERS):colorIndex, -1:DepthBuffer, ignore if using BackBuffer
	 * @param depth   1:R; 2:RG; 3:RGB; 4:RGBA; 5:depth16(ushort); 6:depth24(uint); 7:depth32f(float)
	 * @param y       0:top
	 * @param dataPtr must reserve alignUp(width*depthSize,4)*height unsigned bytes, from top to down, can NOT be null
	 */
	void readFrame(@Nullable Frame frame, int index, int depth, int x, int y, int width, int height, long dataPtr);

	/**
	 * see {@link #readFrame readFrame}
	 */
	void readFrame(@Nullable Frame frame, int index, int depth, int x, int y, int width, int height,
				   @NotNull DownloadPixelBuffer buffer);

	interface Shader {
		int VERTEX = 0;
		int FRAGMENT = 1;

		/**
		 * @return {@link Shader#VERTEX} {@link Shader#FRAGMENT}
		 */
		int type();
	}

	/**
	 * @param type           {@link Shader#VERTEX} {@link Shader#FRAGMENT}
	 * @param shaderTextPtr  can NOT be null
	 * @param shaderTextSize byte size in shaderTextPtr
	 */
	@NotNull Shader compileShader(int type, long shaderTextPtr, long shaderTextSize);

	interface Program {
		@NotNull Class<? extends Model> getModelClass();
	}

	@NotNull Program linkShader(@NotNull Shader vsShader, @NotNull Shader fsShader,
								@NotNull Class<? extends Model> bindModelClass);

	interface ShaderBuffer extends Buffer {
		/**
		 * set current readable range for shader
		 *
		 * @param offset byte offset
		 * @param size   byte size from offset
		 */
		void setDataRange(long offset, long size);
	}

	/**
	 * @param shaderDataPtr  can be null for empty
	 * @param shaderDataSize byte size in shaderDataPtr
	 */
	@NotNull ShaderBuffer createShaderBuffer(long shaderDataPtr, long shaderDataSize);

	class Model {
		public transient @Nullable Program _program;
		public transient @Nullable Mesh _mesh;
		public transient int _index; // current from-offset for index buffer or vertex buffer, in units of vertex
		public transient int _count; // current vertex count, should be multiples of 3 if using DRAW_MODE_TRIANGLES
		public transient int _instance; // set >1 to enable

		public @Nullable Mat4 getMVP() {
			return null;
		}
	}

	void draw(@NotNull Model model);
}
